/*
IM的TCP Client实现，功能有：

. 对外提供Start、Stop进行服务控制；
. 对外提供Send()函数进行数据发送；
. 对外提供IReceivedDataHandler接口实现数据接收回调；
. 内部提供断线重连机制；
.....

日期          作者           说明
----------------------------------------
2018-08-29    lcl           源代码 ver 1.0
+

*/
package com.example.tang5.myapplication.im.Network;

import android.util.Log;

import com.example.tang5.myapplication.im.IMConfiger.IMConfiger;
import com.example.tang5.myapplication.im.IMLoger.IMLoger;
import com.example.tang5.myapplication.im.IMUtil.BytesConverter;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;


//IMTCPClient，IM socket通信主类
public class IMTCPClient {
	private static final String TAG = "IMTCPClient";
	//socket参数
	private String mHost;
	private int mPort;

	//socket
	private Socket mSocket = null;
	//连接的超时时间，毫秒，从Configer读取
	private short mConnectTimeOut = 2000;//默认2s
	//IO stream
	private DataOutputStream mSendStream = null;
	private DataInputStream mReceiveStream = null;
	private boolean mIsConnected = false;
	//IsSending标志，用于send函数互斥，避免并发调用send的引起的问题...
	private boolean mIsSending = false;
	//IsConnecting标志，用于Connect函数互斥
	private boolean mIsConnecting = false;
	//socket读超时
	private short mReadTimeOut = 3000;
	//TCP Client是否运行
	private boolean mClientIsRunning = false;

	//构造
	public IMTCPClient() {
		//参数初始化
		this.mHost = IMConfiger.getInstance().getHost();
		this.mPort = IMConfiger.getInstance().getPort();
		this.mConnectTimeOut = IMConfiger.getInstance().getConnectTimeOut();
		this.mReconnectTimeInterval = IMConfiger.getInstance().getReconnectTimeInterval();
		this.mReadTimeOut = IMConfiger.getInstance().getReadTimeOut();


		//设置TCP连接保持
		try {
			mSocket = new Socket();
			mSocket.setKeepAlive(true);

			//断线重连Timer
			mReconnectTimer = new Timer();

			//实例化数据接收线程
			this.mDataReceiver = new ReceiveThread();
		} catch (SocketException e) {
			Log.e(TAG, "IMTCPClient: ", e);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	//析构
	protected void finalize() {
		if (!mClientIsRunning) {
			this.stop();
		}
		//
		this.stopReconnect();
		mReconnectTimer = null;
	}

	//connect 实例化一个socket，连接到服务器，初始化输入、输出流、更新连接状态
	private boolean connect() {
		//mIsConnecting互斥
		if (mIsConnecting) {
			return false;
		}
		mIsConnecting = true;


		try {
			this.addLog(IMLoger.LOG_TYPE_DEBUG, "IM TCP Client is now connect to server:[" + this.mHost + "/" + this.mPort + "]");
			SocketAddress address = new InetSocketAddress(mHost, mPort);
			mSocket.connect(address, mConnectTimeOut);
			//读超时设置，影响inputStream的read函数
			mSocket.setSoTimeout(this.mReadTimeOut);//
			mSendStream = (DataOutputStream) mSocket.getOutputStream();
			mReceiveStream = (DataInputStream) mSocket.getInputStream();
			this.addLog(IMLoger.LOG_TYPE_DEBUG, "IM TCP Client is now connected to server successed!");

			mIsConnecting = false;
			return true;
		} catch (Exception e) {
			//记录日志
			this.addLog(IMLoger.LOG_TYPE_EXCEPTION, "connect() exception occurred, exception message: " + e.toString());

			mIsConnecting = false;
			return false;
		}

	}

	//isConnected 检测socket是否连接上(注：此函数，并不能完全判断socket连接正常，网络异常断开时，socket可能并未断开)
	public boolean isConnected() {
		return (null != this.mSocket) & this.mSocket.isBound() & this.mSocket.isConnected() & !this.mSocket.isClosed() &
				(null != this.mSendStream) & !this.mSocket.isOutputShutdown() &
				(null != this.mReceiveStream) & !this.mSocket.isInputShutdown();
	}

	private boolean disConnect() {
		try {
			//关发送流
			if (null != mSendStream) {
				mSendStream.close();
			}

			//关接收流
			//todo：关receive以前，要保证收到的数据包，全都处理完，再关
			if (null != mReceiveStream) {
				mReceiveStream.close();
			}

			//socket断开
			if (null != mSocket) {
				mSocket.close();

				Log.d(TAG, "IM TCP Client is disconnected!");
			}
		} catch (Exception e) {
			Log.d(TAG, "disConnect() exception occurred, exception message: " + e.toString());
		}
		return true;
	}

	//TCP Client功能开始和结束
	public void start() {
		Log.d(TAG, "IM TCP Client is now to starting...");
		//socket连接服务器
		boolean bRet = false;
		bRet = this.connect();
		if (bRet) {
			//启动数据接收线程
			mDataReceiver.start();
			mClientIsRunning = true;
			Log.d(TAG, "IM TCP Client is running...");
		}
	}

	public void stop() {
		//先停止数据接收线程，Thread的run函数会读完当时的数据包，并跳出while，结束执行
		//Thread.interrupt()只是发送中断信号，线程的run函数需要cache InterruptedException 异常并跳出，或是调用isInterrupted检查并跳出
		this.mDataReceiver.interrupt();

		//断开socket连接
		this.disConnect();
		mClientIsRunning = false;
		Log.d(TAG, "IM TCP Client is now stoped...");
	}

	//数据发送
	public boolean send(byte[] byteDatas) {
		//send调用互斥.并发调用send，可能产生数据混乱的问题(也可能不会，需要验证)
		if (this.mIsSending) {
			return false;
		}
		this.mIsSending = true;//改标志

		Log.d(TAG, "send() function is called, try to sending datas to server...");
		//如果没有连接上，直接返回false，并激活重连机制
		if (!this.isConnected()) {
			//激活重启机制
			Log.d(TAG, "send() function is called, but socket is disconnected, now call startReconnect() to reconnceting... ");
			this.startReconnect();

			this.mIsSending = false;
			return false;
		}

		//如果是空数据，直接返回false，并记录警告log...
		//空数据判断规则：body为空时的head长度为16，所以，小于16则为无效数据包
		if ((null == byteDatas) & (byteDatas.length < 16)) {
			//to do：16为硬编码，以后改为常量(在IM Message单元提供常量定义)
			Log.d(TAG, "send() function is called, but message body is invalid ，length of body data:" + byteDatas.length);
			this.mIsSending = false;
			return false;
			//logo
			//IMLoger.addWarnning(....);
		}

		//发数据
		try {
			//this.mSendStream.reset();//在向流中添加数据以前，先清空以前的数据
			this.mSendStream.write(byteDatas);
			this.mSendStream.flush();//强制立即发送数据，不等缓冲区满.
			//todo : 调用flush，可能带来性能或其他问题，以后此处可优化

			this.mIsSending = false;
			Log.d(TAG, "send() function is called, datas send is successed!");
			return true;
		} catch (Exception e) {
			Log.d(TAG, "send() exception occurred, exception message: " + e.toString());
			this.mIsSending = false;
			return false;
		}
	}

	;//end of send()

	//数据接收相关-----------------------------------------------------------

	//回调接口
	private IReceivedDataHandler mReceivedDataHandler = null;

	public void setReceivedDataHandler(IReceivedDataHandler handler) {
		this.mReceivedDataHandler = handler;
	}

	//readReceiveStream函数的返回值常量定义
	public static final short SOCKET_READ_RESULT_SUCCESS = 0;  //函数执行成功，并且有读到的数据返回
	public static final short SOCKET_READ_RESULT_NO_MORE_DATA = -1; //DataInputSteam.read函数返回了-1，Socket没有新数据到达；
	public static final short SOCKET_READ_RESULT_PACKAGE_DATA_ERROR = 1;  //读取的数据包格式不对(首4byte问题，或总包长小于16)；
	public static final short SOCKET_READ_RESULT_PREREQUISITE_UNREADY = 2;  //执行的前提条件不足，如DataInputStream为null等
	public static final short SOCKET_READ_RESULT_READ_TIMEOUT = 3;  //read函数超时异常
	public static final short SOCKET_READ_RESULT_OTHER_EXCEPTIONS = 4;  //函数发生了其他异常

	//readReceiveStream 从receiveStream中读取一个数据包，返回执行状态码，和读出的字节数组
	//说明：
	//1、当出现读超时的情况时，即使已经读取了一部分包数据，那么那个包会丢掉；
	//2、不合格的包(总包长小于16，首4byte不是int)，会被直接丢掉；
	//3、根据不同的运行情况，给出了不同返回值，由调用方决定上级的流程；
	private short readReceiveStream(byte[] readedBytes) {

		//执行条件检查
		if ((null != this.mReceiveStream) & !this.mSocket.isInputShutdown()) {
			return IMTCPClient.SOCKET_READ_RESULT_PREREQUISITE_UNREADY;
		}

		int readedBytesNum = 0; //read函数的返回值，表示read读入byte[]的总字节数
		try {
			byte[] packageLenBuffer = new byte[4]; //读取包总长的缓冲区，4字节int
			int packageLen = 0;           //首4 byte的“总包长”
			//读取4byte的总包长
			//阻塞读，直到读到4字节，或引发超时异常，-1表示已达到末尾，没有数据收到
			readedBytesNum = this.mReceiveStream.read(packageLenBuffer, 0, 4);
			if (readedBytesNum == -1) { //读到末尾，没有新数据
				return IMTCPClient.SOCKET_READ_RESULT_NO_MORE_DATA;
			}

			//转int，取总包长：
			packageLen = BytesConverter.byte4ToInt(packageLenBuffer, 0);
			//1、读出的4byte转换不成功，不是一个int，那么是一个非法包，直接返回null
			//2、读出的4byte成功转换为int，但是值小于16，也是非法包；
			if (packageLen < 16) {
				return IMTCPClient.SOCKET_READ_RESULT_PACKAGE_DATA_ERROR;
			}

			//整包缓冲区
			byte[] packageBuffer = new byte[packageLen];
			//先组组装前4byte的“总包长”进buffer
			for (int i = 0; i < packageLenBuffer.length; i++) {
				packageBuffer[i] = packageLenBuffer[i];
			}
			//再从socket中读取本包余下的字节数
			//阻塞读，直到读到余下的字节数，或引发超时异常，-1表示已达到末尾，没有数据收到
			readedBytesNum = this.mReceiveStream.read(packageBuffer, 4, packageLen - 4);
			if (readedBytesNum == -1) { //读到末尾或socket已关，直接返回null
				return IMTCPClient.SOCKET_READ_RESULT_NO_MORE_DATA;
			}

			//再组装余下内容进buffer
			for (int i = 4; i < packageBuffer.length; i++) {
				packageBuffer[i] = packageLenBuffer[i - 4];
			}
			//返回值
			readedBytes = packageBuffer;
			return IMTCPClient.SOCKET_READ_RESULT_SUCCESS;
		} catch (SocketTimeoutException e) {
			//SocketTimeoutException异常，并不会导致socket关闭，只是read()函数超时
			readedBytes = null;
			return IMTCPClient.SOCKET_READ_RESULT_READ_TIMEOUT;
		} catch (SocketException e) {
			//SocketException异常或其他异常，有可能引起socket关闭
			readedBytes = null;
			return IMTCPClient.SOCKET_READ_RESULT_OTHER_EXCEPTIONS;
		} catch (Exception e) {
			readedBytes = null;
			return IMTCPClient.SOCKET_READ_RESULT_OTHER_EXCEPTIONS;
		}


	}

	public boolean isConnecting() {
		return mIsConnecting;
	}


	//接收线程的成员变量，在socket连接成功以后实例化，并启动线程，在socket断开以后结束
	private ReceiveThread mDataReceiver = null;

	//ReceiveThread 在run函数中，循环读取socket收到的数据包，并发起OnDataReceived事件/回调接口;
	class ReceiveThread extends Thread {
		@Override
		public void run() {
			byte[] receivedBytes = null;
			short readResult = 0;
			//while跳出机制 ：外部调用Interrupte()，while内检查isInterrupted()和catch InterruptedException异常
			Log.d(TAG, "ReceiveThread is start running...");
			while (true) {
				try {
					//检查外部的停止信号
					if (this.isInterrupted()) {
						break;
					}

					//从socket中读取一个数据包，每一次取数据，都会产生新的byte[]
					Log.e(TAG, "ReceiveThread is now try to read a package...");
					receivedBytes = null;
					readResult = readReceiveStream(receivedBytes);

					switch (readResult) {
						//成功读了一个包，调用回调接口
						case IMTCPClient.SOCKET_READ_RESULT_SUCCESS: {
							if (null != mReceivedDataHandler) {
								boolean handlerResult;
								handlerResult = mReceivedDataHandler.onDataReceived(receivedBytes);
								Log.d(TAG, "ReceiveThread : a data package is read successed!");
								//todo ：如果回调结果失败过多，是否重新连接socket？
							}
							break;
						}
						//read()函数返回了-1
						case IMTCPClient.SOCKET_READ_RESULT_NO_MORE_DATA:
							//读包时，产生数据不合法的错误
						case IMTCPClient.SOCKET_READ_RESULT_PACKAGE_DATA_ERROR:
							//前置条件不足(socket关了，或DataInputStream为null)
						case IMTCPClient.SOCKET_READ_RESULT_PREREQUISITE_UNREADY:
							//产生了其它异常
						case IMTCPClient.SOCKET_READ_RESULT_OTHER_EXCEPTIONS: {
							Log.d(TAG, "ReceiveThread : package reading failed，result of readReceiveStream() is :" + readResult);

							//检查socket连接是否断开，如果断开，启动重连机制
							if (!isConnected()) {
								Log.e(TAG, "ReceiveThread: package reading failed，the socket is disconnected, now call startReconnect() to reconnceting... ");
								startReconnect();
							}
							break;
						}
						//
						default:
							break;
					}//end of switch
				} /*catch (InterruptedException e) {
					//外部调用Interrupte()引发异常，必须跳出while，结束run函数，此处是线程结束机制，不记录log;
					break;
				} */ catch (Exception e) {
					Log.e(TAG, "ReceiveThread : package reading  exception occurred, exception message: " + e.toString());
					break;
				}
			}//end of while{}

			Log.d(TAG, "ReceiveThread is now stoped!");
		}//end of run{}
	}//end of class ReceiveThread


	/*
	  断线重连相关：
	  1、APP端，网络连接断开以后，会一直重新连接，直到重新连接上服务端为止；
	  2、采用JAVA timer类实现，timer类实际上是以子线程实现，避免重连造成的卡UI；
	  3、只要没有连接上，会一直进行重新连接，所以不设置“重试次数”等参数；
	*/
	//Timer成员
	private Timer mReconnectTimer = null;
	//重连的间隔时间，毫秒，从configer读取
	private short mReconnectTimeInterval;

	//重连操作
	private void startReconnect() {
		//如果socket是连接上的，直接退出
		if (isConnected()) {
			return;
		}

		//给timer添加任务，以mReconnectTimeInterval作为重复频率执行
		mReconnectTimer.scheduleAtFixedRate(new TimerTask() {
			                                    @Override
			                                    public void run() {
				                                    //如果没有处于连接中的状态，则直接调用Connect函数进行连接;
				                                    if (!mIsConnecting) {
					                                    connect();
				                                    }

				                                    //如果已经连接上，就停止断线重连，
				                                    if (isConnected()) {
					                                    stopReconnect();
				                                    }
			                                    }
		                                    }, 500,//500ms后马上执行；
				mReconnectTimeInterval);//timer重复频率
	}

	//取消timer任务
	private void stopReconnect() {
		this.mReconnectTimer.cancel();
	}

	//日志代理函数-----------------------------------------------------------
	//addLog 调用IMLoger的日志接口，并给出一些固定参参数，检查TCP Client的日志输出
	private void addLog(Integer logType, String logText) {
		String curEnvironment = null;
		//todo ：把curEnvironment的内容改写为 手机型号 + OS版本等信息
		Log.d(TAG, new StringBuffer()
				.append(new Date())
				.append("IM TCP Client Class")
				.append(logText)
				.append(curEnvironment)
				.toString());

	}


}